<script setup lang="ts">
import { defineWidget } from "@vue-motion/core";
import type { Growable } from "@vue-motion/lib";
import { Line, Rect, Text, widget } from "@vue-motion/lib";

import { type Ref, inject, ref, watchEffect } from "vue";
import type { DateTimeUnit } from "luxon";
import { DateTime } from "luxon";
import stringWidth from "string-width";
import type { BaseChartOptions, Color, DateTimeFormatOptions } from "..";
import { ColorEnum, DataUtil } from "..";
import type { BaseSimpleChartData } from "./baseSimpleChart.vue";

export interface ChartLayoutOptions extends BaseChartOptions, Growable {}

interface ChartAxis {
  /**
   * Suggested minimum value.
   */
  suggestedMin: number | DateTime;
  /**
   * Suggested maximum value.
   */
  suggestedMax: number | DateTime;
  /**
   * Grid color.
   */
  gridColor: Color;
  /**
   * Grid width.
   */
  gridWidth: number;
  /**
   * Minimum value. Generated by the data and the suggested minimum value.
   */
  min: number;
  /**
   * Maximum value. Generated by the data and the suggested maximum value.
   */
  max: number;
  /**
   * Interval. Generated by the data.
   */
  interval: number;
  /**
   * Positions of the axis labels.
   */
  pos: number[];
  /**
   * Positions of the grid lines and the ticks.
   */
  posLine?: number[];
  /**
   * Label texts.
   */
  labelTexts?: string[];
}

export interface ChartLayoutConfig {
  indexAxis?: "x" | "y";
  indexType?: "label" | "number" | "date";
  index?: ChartAxis;
  cross?: ChartAxis;
  width?: number;
  height?: number;
  indexIntervalUnit?: DateTimeUnit;
  dateFormatOptions?: DateTimeFormatOptions;
  gridAlign?: boolean;
  edgeOffset?: boolean;
  done?: boolean;
}

const props = withDefaults(defineProps<ChartLayoutOptions>(), {
  gridAlign: true,
  edgeOffset: false,
});
const options = defineWidget(props);

const data = inject<Ref<BaseSimpleChartData>>(
  "chartData",
  inject<Ref<BaseSimpleChartData>>(
    "mixedData",
    ref<BaseSimpleChartData>({ datasets: [] }) as Ref<BaseSimpleChartData>,
  ),
);
const layoutConfig = inject<Ref<ChartLayoutConfig>>(
  "chartLayoutConfig",
  ref<ChartLayoutConfig>({}),
);

const legendWidthPrefix = ref([0]);

function generateAxisRange(axis: ChartAxis, data: number[]) {
  const minDataValue = Math.min(
    ...data,
    typeof axis.suggestedMin === "number"
      ? axis.suggestedMin
      : Number.NEGATIVE_INFINITY,
  );
  const maxDataValue = Math.max(
    ...data,
    typeof axis.suggestedMax === "number"
      ? axis.suggestedMax
      : Number.POSITIVE_INFINITY,
  );
  const range = maxDataValue - minDataValue;
  const magnitude = Math.floor(Math.log10(range));
  axis.interval = 10 ** magnitude;

  if (range / axis.interval < 5)
    axis.interval /= Math.ceil(range / axis.interval) === 2 ? 4 : 2;

  axis.min = Math.floor(minDataValue / axis.interval) * axis.interval;
  axis.max = Math.ceil(maxDataValue / axis.interval) * axis.interval;

  for (let i = axis.min; i <= axis.max; i += axis.interval) axis.pos.push(i);
}

function generateDateAxisRange(axis: ChartAxis, data: DateTime[]) {
  const dateTypes: DateTimeUnit[] = [
    "year",
    "month",
    "week",
    "day",
    "hour",
    "minute",
    "second",
    "millisecond",
  ];
  const minDataValue = DateTime.min(
    ...data,
    axis.suggestedMin instanceof DateTime ? axis.suggestedMin : data[0],
  );
  const maxDataValue = DateTime.max(
    ...data,
    axis.suggestedMax instanceof DateTime ? axis.suggestedMax : data[0],
  );
  let range = maxDataValue.diff(minDataValue, dateTypes).toObject();
  axis.interval = 1;
  if (layoutConfig.value.indexIntervalUnit === undefined) {
    for (const dateType of dateTypes) {
      if (range[`${dateType}s`] !== 0) {
        layoutConfig.value.indexIntervalUnit = dateType;
        break;
      }
    }
  }
  while (dateTypes.shift() !== layoutConfig.value.indexIntervalUnit) {
    // Do nothing.
  }
  dateTypes.unshift(layoutConfig.value.indexIntervalUnit!);

  let date = minDataValue.startOf(layoutConfig.value.indexIntervalUnit!);
  range = maxDataValue.diff(date, dateTypes).toObject();
  for (const dateType of dateTypes) {
    if (dateType !== layoutConfig.value.indexIntervalUnit) {
      if (range[`${dateType}s`]! > 0) {
        range[`${layoutConfig.value.indexIntervalUnit!}s`]!++;
        break;
      }
    }
  }
  // if (range[`${this.indexIntervalUnit}s`] < 5)
  //   axis.interval /= (Math.ceil(range[`${this.indexIntervalUnit}s`]) === 2 ? 4 : 2)
  axis.min = minDataValue
    .startOf(layoutConfig.value.indexIntervalUnit!)
    .get(
      layoutConfig.value.indexIntervalUnit !== "week"
        ? layoutConfig.value.indexIntervalUnit!
        : "weekYear",
    );
  axis.max = axis.min + range[`${layoutConfig.value.indexIntervalUnit!}s`]!;

  for (let i = axis.min; i <= axis.max; i += axis.interval) {
    axis.pos.push(i);
    axis.labelTexts!.push(
      date[
        layoutConfig.value.dateFormatOptions![
          layoutConfig.value.indexIntervalUnit!
        ]
      ]!.toString(),
    );
    date = date.plus({
      [layoutConfig.value.indexIntervalUnit!]: axis.interval,
    });
  }
}

watchEffect(
  () => {
    if (data.value.datasets.length === 0) return;
    if (data.value.datasets.some((dataset) => dataset.data.length === 0))
      return;
    if ((data.value.labels ?? []).length === 0) return;

    data.value.datasets.forEach((dataset) => {
      dataset.data.forEach((dataUnit, index) => {
        if (
          !dataUnit.index &&
          data.value.labels &&
          (data.value.labels[index] as DateTime).isValid
        ) {
          dataUnit.index = data.value.labels[index] as DateTime;
        }
      });
    });

    layoutConfig.value.width = options.width ?? 300;
    layoutConfig.value.height = options.height ?? 300;

    layoutConfig.value.indexAxis = options.indexAxis ?? "x";
    layoutConfig.value.indexType =
      (options.indexType ??
      (data.value.labels &&
        data.value.labels.every((label) => typeof label === "string")))
        ? "label"
        : (data.value.labels &&
              data.value.labels.every((label) => label instanceof DateTime)) ||
            data.value.datasets
              .flatMap((set) =>
                set.data.map((unit) => DataUtil.isIndexDate(unit)),
              )
              .every((isDate) => isDate)
          ? "date"
          : "number";
    layoutConfig.value.indexIntervalUnit = options.indexIntervalUnit;
    layoutConfig.value.dateFormatOptions = {
      year: "year",
      quarter: "quarter",
      month: "monthLong",
      week: "weekNumber",
      day: "weekdayLong",
      hour: "hour",
      minute: "minute",
      second: "second",
      millisecond: "millisecond",
      ...options.dateFormatOptions,
    };
    layoutConfig.value.gridAlign = options.gridAlign;
    layoutConfig.value.edgeOffset = options.edgeOffset;

    // indexType validation
    if (layoutConfig.value.indexType === "label" && !data.value.labels)
      throw new Error("indexType is label but labels are not provided");
    if (
      layoutConfig.value.indexType === "date" &&
      data.value.datasets
        .flatMap((set) => set.data.map((unit) => DataUtil.isIndexDate(unit)))
        .some((isDate) => !isDate)
    )
      throw new Error(
        "indexType is date but some index/labels are not DateTimes",
      );
    if (
      layoutConfig.value.indexType === "number" &&
      data.value.datasets
        .flatMap((set) => set.data.map((unit) => DataUtil.isIndexDate(unit)))
        .some((isDate) => isDate)
    )
      throw new Error("indexType is number but some index are DateTimes");

    layoutConfig.value.index = {
      suggestedMin:
        (options.axis?.index?.suggestedMin ??
        options.suggestedMin ??
        options.axis?.index?.beginAtZero ??
        true)
          ? 0
          : Number.POSITIVE_INFINITY,
      suggestedMax:
        options.axis?.index?.suggestedMax ??
        options.suggestedMax ??
        Number.NEGATIVE_INFINITY,
      gridColor:
        options.axis?.index?.gridColor ?? options.gridColor ?? ColorEnum.WHITE,
      gridWidth: options.axis?.index?.gridWidth ?? options.gridWidth ?? 1,
      min: 0,
      max: 0,
      interval: 0,
      pos: [],
      posLine: [],
      labelTexts: [],
    };

    if (layoutConfig.value.indexType === "label") {
      layoutConfig.value.index.min = 0;
      layoutConfig.value.index.max = data.value.labels!.length - 1;
      layoutConfig.value.index.interval = 1;
      for (let i = 0; i < data.value.labels!.length; i++)
        layoutConfig.value.index.pos.push(i);
      layoutConfig.value.index.labelTexts = data.value.labels!.map((label) =>
        label.toString(),
      );
    } else if (layoutConfig.value.indexType === "date") {
      generateDateAxisRange(
        layoutConfig.value.index,
        data.value.datasets.flatMap((set) =>
          set.data.map((unit) => DataUtil.indexDate(unit)),
        ),
      );
      data.value.datasets.forEach((dataset) => {
        dataset.data.forEach((dataUnit) => {
          dataUnit.intervalUnit = layoutConfig.value.indexIntervalUnit;
        });
      });
    } else {
      generateAxisRange(
        layoutConfig.value.index,
        data.value.datasets.flatMap((set) =>
          set.data.map((unit) => DataUtil.indexNumber(unit)),
        ),
      );
    }
    layoutConfig.value.index.posLine = [...layoutConfig.value.index.pos];
    if (layoutConfig.value.gridAlign) {
      layoutConfig.value.index.min -= 0.5 * layoutConfig.value.index.interval;
      layoutConfig.value.index.max += 0.5 * layoutConfig.value.index.interval;
      layoutConfig.value.index.posLine.push(
        layoutConfig.value.index.posLine[
          layoutConfig.value.index.posLine.length - 1
        ] + layoutConfig.value.index.interval,
      );
      layoutConfig.value.index.posLine = layoutConfig.value.index.posLine.map(
        (pos) => pos - 0.5 * layoutConfig.value.index!.interval,
      );
    }
    if (layoutConfig.value.edgeOffset) {
      layoutConfig.value.index.min -= 0.5 * layoutConfig.value.index.interval;
      layoutConfig.value.index.max += 0.5 * layoutConfig.value.index.interval;
      layoutConfig.value.index.posLine.unshift(
        -0.5 * layoutConfig.value.index.interval,
      );
      layoutConfig.value.index.posLine.push(
        layoutConfig.value.index.posLine[
          layoutConfig.value.index.posLine.length - 1
        ] +
          0.5 * layoutConfig.value.index.interval,
      );
    }

    layoutConfig.value.cross = {
      suggestedMin:
        (options.axis?.cross?.suggestedMin ??
        options.suggestedMin ??
        options.axis?.cross?.beginAtZero ??
        true)
          ? 0
          : Number.MAX_VALUE,
      suggestedMax:
        (options.axis?.cross?.suggestedMax ??
        options.suggestedMax ??
        options.axis?.cross?.beginAtZero ??
        true)
          ? Number.MIN_VALUE
          : 0,
      gridColor:
        options.axis?.cross?.gridColor ?? options.gridColor ?? ColorEnum.WHITE,
      gridWidth: options.axis?.cross?.gridWidth ?? options.gridWidth ?? 1,
      min: 0,
      max: 0,
      interval: 0,
      pos: [],
    };

    generateAxisRange(
      layoutConfig.value.cross,
      data.value.datasets.flatMap((set) => set.data.map((unit) => unit.cross)),
    );

    for (let i = 1; i <= data.value.datasets.length; i++) {
      legendWidthPrefix.value[i] = legendWidthPrefix.value[i - 1];
      legendWidthPrefix.value[i] +=
        stringWidth(data.value.datasets[i - 1].label) * 12;
      if (i < data.value.datasets.length) legendWidthPrefix.value[i] += 36;
    }

    data.value.datasets.forEach((dataset) => {
      dataset.data.forEach((dataUnit, index) => {
        if (
          !(!DataUtil.isIndexDate(dataUnit) && dataUnit.index) &&
          data.value.labels &&
          typeof data.value.labels[index] === "string"
        )
          dataUnit.index = layoutConfig.value.index!.pos[index];
      });
    });

    layoutConfig.value.done = true;
  },
  {
    flush: "post",
  },
);
</script>

<template>
  <g v-if="layoutConfig.done" v-bind="widget(options)">
    <!-- Index Axis -->
    <Line
      :from="
        layoutConfig.indexAxis === 'x'
          ? [0, layoutConfig.height!]
          : [0, layoutConfig.height!]
      "
      :to="
        layoutConfig.indexAxis === 'x'
          ? [layoutConfig.width!, layoutConfig.height!]
          : [0, 0]
      "
      :stroke="layoutConfig.index?.gridColor"
      :stroke-width="layoutConfig.index?.gridWidth"
      stroke-opacity="0.6"
    />
    <!-- Cross Axis -->
    <Line
      :from="
        layoutConfig.indexAxis === 'x'
          ? [0, layoutConfig.height!]
          : [0, layoutConfig.height!]
      "
      :to="
        layoutConfig.indexAxis === 'x'
          ? [0, 0]
          : [layoutConfig.width!, layoutConfig.height!]
      "
      :stroke="layoutConfig.cross?.gridColor"
      :stroke-width="layoutConfig.cross?.gridWidth"
      stroke-opacity="0.6"
    />
    <!-- Index Labels -->
    <g v-for="(pos, index) in layoutConfig.index?.pos" :key="index">
      <Text
        v-if="layoutConfig.indexAxis === 'x'"
        :x="
          ((pos - layoutConfig.index!.min) /
            (layoutConfig.index!.max - layoutConfig.index!.min)) *
          layoutConfig.width!
        "
        :y="layoutConfig.height! + 16"
        text-anchor="middle"
        :fill="layoutConfig.index!.gridColor"
        font-size="16"
      >
        {{ layoutConfig.index!.labelTexts![index] ?? pos }}
      </Text>
      <Text
        v-else
        :x="-8"
        :y="
          layoutConfig.height! +
          2 -
          ((pos - layoutConfig.index!.min) /
            (layoutConfig.index!.max - layoutConfig.index!.min)) *
            layoutConfig.height!
        "
        text-anchor="end"
        :fill="layoutConfig.index!.gridColor"
        font-size="16"
      >
        {{ layoutConfig.index!.labelTexts![index] ?? pos }}
      </Text>
    </g>
    <!-- Cross Labels -->
    <g v-for="(pos, index) in layoutConfig.cross?.pos" :key="index">
      <Text
        v-if="layoutConfig.indexAxis === 'x'"
        :x="-8"
        :y="
          layoutConfig.height! +
          2 -
          ((pos - layoutConfig.cross!.min) /
            (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
            layoutConfig.height!
        "
        text-anchor="end"
        :fill="layoutConfig.cross!.gridColor"
        font-size="16"
      >
        {{ pos }}
      </Text>
      <Text
        v-else
        :x="
          ((pos - layoutConfig.cross!.min) /
            (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
          layoutConfig.width!
        "
        :y="layoutConfig.height! + 16"
        text-anchor="middle"
        :fill="layoutConfig.cross!.gridColor"
        font-size="16"
      >
        {{ pos }}
      </Text>
    </g>
    <!-- Index Ticks -->
    <g v-for="(pos, index) in layoutConfig.index?.posLine" :key="index">
      <Line
        v-if="layoutConfig.indexAxis === 'x'"
        :from="[
          ((pos - layoutConfig.index!.min) /
            (layoutConfig.index!.max - layoutConfig.index!.min)) *
            layoutConfig.width!,
          layoutConfig.height!,
        ]"
        :to="[
          ((pos - layoutConfig.index!.min) /
            (layoutConfig.index!.max - layoutConfig.index!.min)) *
            layoutConfig.width!,
          layoutConfig.height! + 5,
        ]"
        :stroke="layoutConfig.index!.gridColor"
        :stroke-width="layoutConfig.index!.gridWidth"
        stroke-opacity="0.3"
      />
      <Line
        v-else
        :from="[
          0,
          layoutConfig.height! -
            ((pos - layoutConfig.index!.min) /
              (layoutConfig.index!.max - layoutConfig.index!.min)) *
              layoutConfig.height!,
        ]"
        :to="[
          -5,
          layoutConfig.height! -
            ((pos - layoutConfig.index!.min) /
              (layoutConfig.index!.max - layoutConfig.index!.min)) *
              layoutConfig.height!,
        ]"
        :stroke="layoutConfig.index!.gridColor"
        :stroke-width="layoutConfig.index!.gridWidth"
        stroke-opacity="0.3"
      />
    </g>
    <!-- Cross Ticks -->
    <g v-for="(pos, index) in layoutConfig.cross?.pos" :key="index">
      <Line
        v-if="layoutConfig.indexAxis === 'x'"
        :from="[
          0,
          layoutConfig.height! -
            ((pos - layoutConfig.cross!.min) /
              (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
              layoutConfig.height!,
        ]"
        :to="[
          -5,
          layoutConfig.height! -
            ((pos - layoutConfig.cross!.min) /
              (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
              layoutConfig.height!,
        ]"
        :stroke="layoutConfig.cross!.gridColor"
        :stroke-width="layoutConfig.cross!.gridWidth"
        stroke-opacity="0.3"
      />
      <Line
        v-else
        :from="[
          ((pos - layoutConfig.cross!.min) /
            (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
            layoutConfig.width!,
          layoutConfig.height!,
        ]"
        :to="[
          ((pos - layoutConfig.cross!.min) /
            (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
            layoutConfig.width!,
          layoutConfig.height! + 5,
        ]"
        :stroke="layoutConfig.cross!.gridColor"
        :stroke-width="layoutConfig.cross!.gridWidth"
        stroke-opacity="0.3"
      />
    </g>
    <!-- Index Grids -->
    <g v-for="(pos, index) in layoutConfig.index?.posLine" :key="index">
      <Line
        v-if="layoutConfig.indexAxis === 'x'"
        :from="[
          ((pos - layoutConfig.index!.min) /
            (layoutConfig.index!.max - layoutConfig.index!.min)) *
            layoutConfig.width!,
          layoutConfig.height!,
        ]"
        :to="[
          ((pos - layoutConfig.index!.min) /
            (layoutConfig.index!.max - layoutConfig.index!.min)) *
            layoutConfig.width!,
          0,
        ]"
        :stroke="layoutConfig.index!.gridColor"
        :stroke-width="layoutConfig.index!.gridWidth"
        stroke-opacity="0.3"
      />
      <Line
        v-else
        :from="[
          0,
          layoutConfig.height! -
            ((pos - layoutConfig.index!.min) /
              (layoutConfig.index!.max - layoutConfig.index!.min)) *
              layoutConfig.height!,
        ]"
        :to="[
          layoutConfig.width!,
          layoutConfig.height! -
            ((pos - layoutConfig.index!.min) /
              (layoutConfig.index!.max - layoutConfig.index!.min)) *
              layoutConfig.height!,
        ]"
        :stroke="layoutConfig.index!.gridColor"
        :stroke-width="layoutConfig.index!.gridWidth"
        stroke-opacity="0.3"
      />
    </g>
    <!-- Cross Grids -->
    <g v-for="(pos, index) in layoutConfig.cross?.pos" :key="index">
      <Line
        v-if="layoutConfig.indexAxis === 'x'"
        :from="[
          0,
          layoutConfig.height! -
            ((pos - layoutConfig.cross!.min) /
              (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
              layoutConfig.height!,
        ]"
        :to="[
          layoutConfig.width!,
          layoutConfig.height! -
            ((pos - layoutConfig.cross!.min) /
              (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
              layoutConfig.height!,
        ]"
        :stroke="layoutConfig.cross!.gridColor"
        :stroke-width="layoutConfig.cross!.gridWidth"
        stroke-opacity="0.3"
      />
      <Line
        v-else
        :from="[
          ((pos - layoutConfig.cross!.min) /
            (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
            layoutConfig.width!,
          layoutConfig.height!,
        ]"
        :to="[
          ((pos - layoutConfig.cross!.min) /
            (layoutConfig.cross!.max - layoutConfig.cross!.min)) *
            layoutConfig.width!,
          0,
        ]"
        :stroke="layoutConfig.cross!.gridColor"
        :stroke-width="layoutConfig.cross!.gridWidth"
        stroke-opacity="0.3"
      />
    </g>
    <!-- Legends -->
    <g v-for="(dataset, index) in data.datasets" :key="index">
      <Text
        :x="
          layoutConfig.width! / 2 -
          legendWidthPrefix[data.datasets.length] / 2 +
          legendWidthPrefix[index] +
          24
        "
        :y="-16"
        width="100"
        text-anchor="left"
        :fill="layoutConfig.index!.gridColor"
        font-size="16"
      >
        {{ dataset.label }}
      </Text>
      <Rect
        :x="
          layoutConfig.width! / 2 -
          legendWidthPrefix[data.datasets.length] / 2 +
          legendWidthPrefix[index]
        "
        :y="-26"
        :width="20"
        :height="16"
        :fill="
          dataset.style?.backgroundColor ??
          dataset.data[0].style?.backgroundColor ??
          ColorEnum.WHITE
        "
        fill-opacity="0.3"
        :border-color="
          dataset.style?.borderColor ??
          dataset.data[0].style?.borderColor ??
          ColorEnum.WHITE
        "
        :border-width="
          dataset.style?.borderWidth ?? dataset.data[0].style?.borderWidth ?? 1
        "
      />
    </g>
  </g>
</template>

<style scoped></style>
